#!/usr/bin/perl
# cybozu2ical: Convert Cybozu Office calendar into iCalendar format
#
# $Id: cybozu2ical 515 2008-08-29 15:58:52Z ogawa $

use strict;
use lib 'lib';

use Encode qw( decode_utf8 encode );
use Data::ICal;
use Data::ICal::Entry::Event;
use Data::ICal::Entry::TimeZone;
use Data::ICal::Entry::TimeZone::Standard;
use DateTime;
use WWW::CybozuOffice6::Calendar;
use URI;
use Pod::Usage;
use Getopt::Long;

our $VERSION = '0.31';

###
### TRICK (stop escaping for 'exdate' property)
###
*Data::ICal::Property::_value_as_string = sub {
    my $self  = shift;
    my $key   = shift;
    my $value = defined( $self->value() ) ? $self->value() : '';

    unless ( $self->vcal10 ) {
        my $lc_key = lc($key);
        $value =~ s/\\/\\/gs;
        $value =~ s/\Q;/\\;/gs
          unless ( $lc_key eq 'rrule' || $lc_key eq 'exdate' );
        $value =~ s/,/\\,/gs
          unless ( $lc_key eq 'rrule' || $lc_key eq 'exdate' );
        $value =~ s/\n/\\n/gs;
        $value =~ s/\\N/\\N/gs;
    }

    return $value;

};

###
### Utility subroutines
###
sub to_icaldate {
    my ( $dt, $is_full_day ) = @_;
    $is_full_day
      ? $dt->ymd('')
      : $dt->ymd('') . 'T'
      . $dt->hms('')
      . ( $dt->time_zone->is_utc ? 'Z' : '' );
}

sub encode_string {
    my ( $enc, $text ) = @_;
    if ( $enc eq 'ncr' ) {
        $text =~ s/(\P{ASCII})/sprintf("&#%d;", ord($1))/eg;
    }
    else {
        $text = encode( $enc, $text );
    }
    $text;
}

sub read_yaml {
    my $file = shift;
    my $yaml;
    if ( eval('require YAML::Tiny') ) {
        ($yaml) = YAML::Tiny::LoadFile($file);
    }
    elsif ( eval('require YAML') ) {
        ($yaml) = YAML::LoadFile($file);
    }
    if ($@) {
        die "Faild to read yaml file: $@";
    }
    $yaml;
}

###
### Main part
###

# Handle command-line options
my %opt = (
    conf                     => 'config.yaml',
    'compat-google-calendar' => 0,
    'uid'                    => 1,
    'url'                    => 0,
);
GetOptions( \%opt, 'output=s', 'conf=s', 'compat-google-calendar', 'debug',
    'input-csv=s', 'output-csv=s', 'help', 'uid!', 'url!' )
  or pod2usage(2);
pod2usage(1) if $opt{help};

# Read configuration file
my $cfg = read_yaml( $opt{conf} );
my $time_zone = $cfg->{time_zone} || 'Asia/Tokyo';

# Generate a root 'calendar' object
my $vcalendar = Data::ICal->new();
$vcalendar->add_properties(
    prodid   => "-//as-is.net/Cybozu2ICal $VERSION//EN",
    calscale => 'GREGORIAN',
    method   => 'PUBLISH',
    $cfg->{calname} ? ( 'X-WR-CALNAME' => $cfg->{calname} ) : (),
    'X-WR-TIMEZONE' => $time_zone
);

# Obtain Cybozu Office 6/7 Calendar items
my $cal = WWW::CybozuOffice6::Calendar->new(%$cfg);

if ( $opt{'input-csv'} ) {
    $cal->read_from_csv_file( $opt{'input-csv'} )
      or die "Failed to read CSV file: $opt{'input-csv'}";
}
else {
    $cal->request()
      or die "Failed to get Cybozu Office 6 Calendar";
}

# Output the calendar CSV for debugging
if ( $opt{'output-csv'} ) {
    local *FH;
    open FH, ">$opt{'output-csv'}" or die "Failed to write $opt{'output-csv'}";
    print FH "$_\n" for $cal->response;
    close FH;
}

# For each items, generate an 'event entry' and append it to the calendar
for my $item ( $cal->get_items() ) {
    my $vevent = Data::ICal::Entry::Event->new();
    my %args   = (
        summary     => decode_utf8( $item->summary ),
        description => decode_utf8( $item->description ),
        created     => to_icaldate( $item->created ),
        dtstamp     => to_icaldate( $item->modified ),
        $opt{url}
        ? ( url => $cal->url . '?page=ScheduleView&EID=' . $item->id )
        : (),
    );

    $args{comment} = decode_utf8( $item->comment )
      if $opt{debug} && $item->comment;

    if ( $item->is_full_day ) {
        $args{dtstart} =
          [ to_icaldate( $item->start, 1 ), { VALUE => 'DATE' } ];
        $args{dtend} = [ to_icaldate( $item->end, 1 ), { VALUE => 'DATE' } ];
    }
    else {
        $args{dtstart} =
          [ to_icaldate( $item->start, 0 ), { TZID => $time_zone } ];
        $args{dtend} = [ to_icaldate( $item->end, 0 ), { TZID => $time_zone } ];
    }

    # handle frequency
    if ( $item->can('rrule') ) {

        # rrule
        my %rrule = %{ $item->rrule };
        $rrule{UNTIL} = to_icaldate( $rrule{UNTIL}, $item->is_full_day )
          if $rrule{UNTIL};
        $rrule{WKST} = 'SU'
          if $opt{'compat-google-calendar'};

        my @rrule_list;
        for (qw(FREQ COUNT INTERVAL BYMONTH BYMONTHDAY WKST BYDAY UNTIL)) {
            push @rrule_list, $_ . '=' . $rrule{$_}
              if exists $rrule{$_};
        }
        $args{rrule} = join ';', @rrule_list;

        # exdate
        if ( $item->exdates ) {
            if ( $item->is_full_day ) {
                my $exdate = join ',',
                  map { to_icaldate( $_, 1 ) } $item->exdates;
                $args{exdate} = [ $exdate, { VALUE => 'DATE' } ];
            }
            else {
                my $exdate = join ',',
                  map { to_icaldate( $_, 0 ) } $item->exdates;
                $args{exdate} = [ $exdate, { TZID => $time_zone } ];
            }
        }

    }

    # set uid (recommended to be the identical syntax to RFC822)
    if ( $opt{uid} ) {
        my $host = URI->new( $cal->url )->host || 'localhost';
        $args{uid} = $args{created} . '-' . $item->id . '@' . $host;
    }

    $vevent->add_properties(%args);
    $vcalendar->add_entry($vevent);
}

# Generate a 'timezone entry' and append it to the calendar
my $vtimezone = Data::ICal::Entry::TimeZone->new();
$vtimezone->add_properties( tzid => $time_zone );

# probably we need to support the Daylight Saving Time, but not yet supported.
my $standard = Data::ICal::Entry::TimeZone::Standard->new();
my $std      = DateTime->new(
    year      => 1970,
    month     => 1,
    day       => 1,
    hour      => 0,
    minute    => 0,
    second    => 0,
    time_zone => $time_zone
);
my $offset = DateTime::TimeZone::offset_as_string( $std->offset ) || '+0900';
my $tzname = $cfg->{tzname} || 'JST';

$standard->add_properties(
    tzoffsetfrom => $offset,
    tzoffsetto   => $offset,
    tzname       => $tzname,
    dtstart      => to_icaldate($std)
);
$vtimezone->add_entry($standard);

$vcalendar->add_entry($vtimezone);

# Outputs the calendar as a string
my $text =
  encode_string( $cfg->{output_encoding} || 'utf8', $vcalendar->as_string );
if ( $opt{output} ) {
    local *FH;
    open FH, ">$opt{output}" or die "Failed to write $opt{output}";
    print FH $text;
    close FH;
}
else {
    print $text;
}

1;
__END__

=head1 NAME

cybozu2ical - Convert Cybozu Office calendar into iCalendar format

=head1 SYNOPSIS

  % cybozu2ical
  % cybozu2ical --conf /path/to/config.yaml

=head1 DESCRIPTION

C<cybozu2ical> is a command line application that fetches calendar
items from Cybozu Office 6 or later, and converts them into an
iCalendar file.  It allows you to easily integrate the Cybozu Calendar
into iCalendar-enabled Calendar applications, such as Microsoft
Outlook, Apple iCal, and of course, Google Calendar.

You can run this via crontab, for example, every 1 hour.

=head1 REQUIREMENT

This application requires perl 5.8.0 with following Perl modules
installed on your box.

=over 4

=item WWW::CybozuOffice6::Calendar

=item Text::CSV_XS or Text::CSV

=item DateTime

=item LWP::UserAgent

=item Class::Accessor::Fast

=item Data::ICal

=item YAML or YAML::Tiny

=back

=head1 OPTIONS

=over 4

=item --output /path/to/output.ics

Specify the output file.  By default, this application outputs to
STDOUT.

=item --conf /path/to/config.yaml

Specify the configuration file.  By default, C<config.yaml> in the
current directory will be used.

=item --compat-google-calendar

Output an iCalendar file compatible with Google Calendar.

=item --debug

Output CSV data in a COMMENT field of each events.  It's just for
debugging.

=item --input-csv /path/to/input.csv

Instead of requesting Cybozu Office 6 server, read from a local CSV
file.

=item --output-csv /path/to/output.csv

Specify the output CSV file for debugging.

=item --uid, --no-uid

Enable/Disable UID fields of the iCalendar file. (Default: Enable)

=item --url, --no-url

Enable/Disable URL fields of the iCalendar file. (Default: Disable)

=item --help

Print out this message.

=back

=head1 CONFIGURATION

The distributions includes a sample configuration file
C<config.yaml.sample>. You can rename it to C<config.yaml> and
configure C<cybozu2ical>.

=over 4

=item cybozu_url

Set the URL of your Cybozu Office 6 or later.

=item calname

Set the calendar name string. iCalendar applications which properly
handle X-WR-CALNAME header, is expected to use this string as a
calendar name.

=item username, userid

Set your username or userid for Cybozu Office.

=item password

Set your password for Cybozu Office.

=item time_zone

Set the timezone of your Cybozu Office (e.g., Asia/Tokyo).

=item tzname

Set the short timezone name of your Cybozu Office (e.g., JST).

=item input_encoding

Set the charset of Cybozu Office. By default, C<input_encoding> is
"shiftjis".

=item output_encoding

Set the charset of the iCalendar file.  By default, C<output_encoding>
is "utf8".  If you need to output multibyte strings as Numeric
Character References for some reason, set C<output_encoding> to "ncr".

=item calendar_driver

Set the calendar driver that C<cybozu2ical> employs.  By default,
C<ApiCalendar> is used as C<calendar_driver>.

Currently, C<ApiCalendar> and C<SyncCalendar> drivers are shipped with
C<cybozu2ical>.  If you are using Cybozu Office 6, C<SyncCalendar> is
strongly recommended.  Otherwise, you have to use C<ApiCalendar>.

=item date_range (experimental)

Set the date range of calendar, which means C<cybozu2ical> handles
calendar items between N days before and after.  Default C<date_range>
is 30.

=back

=head1 DEVELOPMENT

The development version is always available from the following
subversion repository:

  http://code.as-is.net/svn/public/cybozu2ical/trunk/

You can browse the files via Trac from the following:

  http://code.as-is.net/public/browser/cybozu2ical/trunk/

Any comments, suggestions, or patches are welcome.

=head1 LICENSE

Copyright (c) 2008 Hirotaka Ogawa E<lt>hirotaka.ogawa at gmail.comE<gt>.
All rights reserved.

This library is free software; you can redistribute it and/or modify
it under the terms of either:
       
   a) the GNU General Public License as published by the Free Software
      Foundation; either version 1, or (at your option) any later
      version, or
                         
   b) the "Artistic License" which comes with Perl.

=cut
